"""
Module to support Data Base Operations.

Data Base operations are transactional and can be rolled back. Every data
the DB reads is shared locked and every write takes an exclusive lock.

Deadlocks are avoided using timeout.


Last Modified: $Id: db.py 31 2005-05-31 20:02:02Z gustavo $

@author: U{Gustavo Sverzut Barbieri <barbieri@gmail.com>}
@license: GNU GPL
@copyright: (C) 2005 Gustavo Sverzut Barbieri
@version: $Rev: 31 $
"""










import rawdb
import datamanager
import transactionmanager
from debug import debug

class DBException( Exception ):
    """Data Base Exception."""
    pass
# DBException





class DBInvalidId( DBException ):
    """Data Base Invalid ID Exception.

    This exception is raised if tried to access an ID not in DB.
    """
    def __init__( self, exception ):
	self.exception = exception
	self.id        = exception.id
    # __init__()

    def __str__( self ):
	return "%s: given id '%s' is not in data base." % \
	       ( self.__class__.__name__, self.id )
    # __str__()
# DBInvalidId




class DBDeadlockException( DBException ):
    """Data Base Dead-Lock Exception.

    This exception is raised if a deadlock was detected or if a lock cannot
    be acquired.
    """
    def __init__( self, exception ):
	self.exception = exception
    # __init__()

    def __str__( self ):
	return "%s: %s" % \
	       ( self.__class__.__name__, self.exception )
    # __str__()
# DBDeadlockException





class DBNoTransactionException( DBException ):
    """Data Base No-Transaction Exception.

    This exception is raised if operations were made before a transaction
    was started.
    """
    pass
# DBNoTransactionException





class DB( object ):
    """High Level Data Base

    This Data Base handle Transactions, locks and everything else.


    Creating a DB (each tread should have it's own instance!):
    (avoid using singleton, give explicit new RawDB)

    >>> rdb = rawdb.RawDB()
    >>> db = DB( rdb )
    >>> print db
    Data Base:
    RawDB:
        <Empty>


    Before everything, you must begin a transaction:

    >>> db.begin()


    Access to data is easy:

    >>> db.set( 10, 10000 )
    >>> db.get( 10 )
    10000

    >>> db[ 1 ] = 10
    >>> db[ 1 ]
    10
    >>> db[ 10 ]
    10000


    After you're done, you must commit or abort:

    >>> db.commit()

    >>> db.begin()
    >>> db[ 1 ]
    10
    >>> db[ 1 ] = 20
    >>> db[ 1 ]
    20
    >>> db.abort()

    >>> db.begin()
    >>> db[ 1 ]
    10
    >>> db.commit()


    If you didn't commit and the object is destroyed, it's abort()ed.

    >>> db.begin()
    >>> db[ 1 ]
    10
    >>> db[ 1 ] = 20
    >>> db[ 1 ]
    20
    >>> del db # It's aborted!

    >>> db = DB( rdb )
    >>> db.begin()
    >>> db[ 1 ]
    10
    >>> db.commit()

    >>> db.begin()
    >>> db[ 1 ] = 1000
    >>> db.commit()

    >>> db.begin()
    >>> db[ 1 ]
    1000
    >>> db.abort()


    Operations without a transaction raise L{DBNoTransactionException}:

    >>> db[ 1 ]
    Traceback (most recent call last):
       ...
    DBNoTransactionException


    If an ID is not in DB, L{DBInvalidId} is raised:

    >>> db.begin()
    >>> db[ 1234 ]
    Traceback (most recent call last):
       ...
    DBInvalidId: DBInvalidId: given id '1234' is not in data base.
    >>> db.abort()


    @see: L{rawdb.RawDB}
    """

    def __init__( self, data=None ):
        """Create a new DB.

        @param data: raw data base.
        @type  data: L{rawdb.RawDB}

        It will get singleton instances of:
         - L{datamanager.DataManager}: coordinate access to data using locks.
         - L{rawdb.RawDB}: hold data.
         - L{transactionmanager.TransactionManager}: coordinate transactions,
           do commits and aborts/rollbacks.

        @rtype: L{DB}
        """
	self.__tid = None

        if data is None:
            data = rawdb.singleton()

	self.__dm = datamanager.singleton()
	self.__tm = transactionmanager.singleton()
        self.__db = data
        self.__tm.set_db( data )
    # __init__()


    def status( self ):
        return [ str( self.__dm ),
                 str( self.__tm ),
                 str( self.__db ) ]
    # status()




    def __del__( self ):
        """Destroy a DB instance.

        It will abort any left transactions.
        """
	if self.__tid is not None:
	    self.abort()
    # __del__()


    def begin( self ):
        """Begin a transaction.

        This method must be called befor any data access, otherwise
        L{DBNoTransactionException} will be raised.
        """
	if not self.in_transaction():
	    self.__tid = self.__tm.begin()
    # begin()


    def in_transaction( self ):
        """Checks if there is a transaction.

        @return: True if a transaction was already started.
        @rtype: L{bool}
        """
	return self.__tid is not None
    # in_transaction()


    def __check_if_tid( self ):
        """Internal method to raise exception if there is no transaction.

        If there is no transaction, raises L{DBNoTransactionException}.

        @raises DBNoTransactionException: if there is no transaction.
        """
	if self.__tid is None:
	    raise DBNoTransactionException
    # __check_if_tid()


    def end( self ):
        """Finish a transaction, commit changed data.

        @precondition: L{in_transaction}() must be true.
        @raise DBNoTransactionException: if there is no transaction.
        """
	self.__check_if_tid()
	self.__tm.end( self.__tid )
	self.__dm.unlock_all( self.__tid )
	self.__tid = None
    # end()
    commit = end


    def abort( self ):
        """Finish a transaction, rollback changed data.

        @precondition: L{in_transaction}() must be true.
        @raise DBNoTransactionException: if there is no transaction.
        """
	self.__check_if_tid()
	self.__dm.unlock_all( self.__tid )
	self.__tm.abort( self.__tid )
	self.__tid = None
    # abort()


    def get( self, id ):
        """Get value from C{id}.

        @precondition: L{in_transaction}() must be true.
        @requires: C{id} being in Data Base.

        @param id: an object in database.

        @return: value in database for object C{id}.

        @raise DBDeadlockException: if cannot acquire read (shared) lock.
        @raise DBNoTransactionException: if there is no transaction.
        @raise DBInvalidId: if C{id} is not in data base.

        @attention: if it cannot acquire lock, transaction is aborted.
        """
	self.__check_if_tid()

        try:
            if self.slock( id ):
                sc = self.__tm.get_shallow_copy( self.__tid )
                try:
                    return sc.get( id )
                except rawdb.RawDBInvalidId, e:
                    self.__dm.unlock( self.__tid, id )
                    raise DBInvalidId( e )
            else:
                self.abort()
                debug( 2, "cannot acquire shared lock of %s" % id )
        except DBDeadlockException, e:
            self.abort()
            debug( 2, str( e ) )
            raise e
    # get()
    read = get
    __getitem__ = get


    def set( self, id, value ):
        """Set C{value} for C{id} in data base.

        @precondition: L{in_transaction}() must be true.

        @param id: any object to serve as identifier for C{value}.
        @param value: any object to be stored.

        @raise DBDeadlockException: if cannot acquire read (shared) lock.
        @raise DBNoTransactionException: if there is no transaction.

        @attention: if it cannot acquire lock, transaction is aborted.
        """
	self.__check_if_tid()

        try:
            if self.xlock( id ):
                sc = self.__tm.get_shallow_copy( self.__tid )
                return sc.set( id, value )
            else:
                self.abort()
                debug( 2, "cannot acquire exclusive lock of %s" % id )
        except DBDeadlockException, e:
            self.abort()
            debug( 2, str( e ) )
            raise e
    # set()
    write = set
    __setitem__ = set


    def slock( self, id ):
        """Explicitly require read (shared) lock.

        @precondition: L{in_transaction}() must be true.

        @raise DBDeadlockException: if cannot acquire read (shared) lock.
        @raise DBNoTransactionException: if there is no transaction.

        @return: True if lock was acquired.
        @rtype: L{bool}
        """
	self.__check_if_tid()
        try:
            return self.__dm.slock( self.__tid, id )
	except datamanager.DataManagerDeadlock, e:
            debug( 1, "Cannot get SHARED lock:\n%s" % self.__dm )
	    raise DBDeadlockException( e )
    # slock()


    def xlock( self, id ):
        """Explicitly require write(exclusive) lock.

        @precondition: L{in_transaction}() must be true.

        @raise DBDeadlockException: if cannot acquire write (exclusive) lock.
        @raise DBNoTransactionException: if there is no transaction.

        @return: True if lock was acquired.
        @rtype: L{bool}
        """
	self.__check_if_tid()
        try:
            return self.__dm.xlock( self.__tid, id )
	except datamanager.DataManagerDeadlock, e:
            debug( 1, "Cannot get EXCLUSIVE lock:\n%s" % self.__dm )
	    raise DBDeadlockException( e )
    # xlock()


    def __db_load__( self, file ):
        """Load data from C{file} into data base.

        @param file: where to load data.
        @type  file: filename (L{str}) or L{file<__builtin__.file>} object.
                     Look at L{rawdb.RawDB.load}
        @see: L{rawdb.RawDB.load}

        @warning: Use with care! No kind of transaction or data manager are
        used. Data is load directly into internal DB.
        """
	self.__db.load( file )
    # __db_load__()


    def __db_save__( self, file ):
        """Save data from data base to C{file}.

        @param file: where to save data.
        @type  file: filename (L{str}) or L{file<__builtin__.file>} object.
                     Look at L{rawdb.RawDB.save}
        @see: L{rawdb.RawDB.save}
        """
	self.__db.save( file )
    # __db_save__()


    def __db_update__( self, data ):
	"""Load data into database.

        @param data: data to put in data base.
        @type  data: L{dict}

        @warning: Use with care! No kind of transaction or data manager are
        used. Data is loaded directly into internal DB.

        @see: L{rawdb.RawDB.update}
        """
	self.__db.update( data )
    # __db_update__()


    def __db_clear__( self ):
        """Clear data base contents.

        @warning: Use with care! No kind of transaction or data manager are
        used. Data is wiped directly from internal DB.

        @see: L{rawdb.RawDB.clear}
        """
        self.__db.clear()
    # __db_clear__()


    def __str__( self ):
        """DB representation.

        @rtype: L{str}
        """
        return "Data Base:\n%s" % self.__db
    # __str__()
# DB










class VersionedDB( DB ):
    """Versioned Data Base.

    This data base is versioned, each update to data increments version number.


    Creating a Versioned DB (avoid using singleton, give explicit new RawDB):

    >>> vdb = VersionedDB( rawdb.RawDB() )
    >>> vdb.get_version()
    0


    Add data do DB, so it increases version:

    >>> vdb.begin()
    >>> vdb.set( 1, 10 )
    >>> vdb.end()
    >>> vdb.get_version()
    1

    Read data doesn't increase version:

    >>> vdb.begin()
    >>> vdb.get( 1 )
    10
    >>> vdb.end()
    >>> vdb.get_version()
    1


    Obtain diferences from different versions:

    >>> vdb.begin()
    >>> vdb.set( 1, 100 )
    >>> vdb.set( 2, 200 )
    >>> vdb.end()
    >>> vdb.get_version()
    2
    >>> vdb.diff( 0 )
    {1: 100, 2: 200}

    >>> vdb.diff( 1, 2 )
    {1: 100, 2: 200}

    >>> vdb.begin()
    >>> vdb[ 2 ] = 300
    >>> vdb.end()
    >>> vdb.diff( 2, 3 )
    {2: 300}

    >>> vdb.diff( 1, 3 )
    {1: 100, 2: 300}


    You may also clean up journal, freeing memory. But be careful, you cannot
    use diff() for forgotten versions:

    >>> vdb.get_version()
    3
    >>> vdb.shrink_journal( 2 )
    >>> vdb.get_version()
    3
    >>> vdb.diff( 0, 1 ) # empty, since data was lost
    {}
    >>> vdb.diff( 1, 2 ) # like previous execution
    {1: 100, 2: 200}
    >>> vdb.diff( 2, 3 ) # also like previous exec.
    {2: 300}


    You may convert from DB to VersionedDB, just share it's internal db:

    >>> rdb = rawdb.RawDB()
    >>> db = DB( rdb )
    >>> db.begin()
    >>> db.set( 1, 10 )
    >>> db.get( 1 )
    10
    >>> db.end()

    >>> journal_log = [ ( ( 1, 10 ), ) ]

    >>> vdb = VersionedDB( rdb, journal_log )
    >>> vdb.begin()
    >>> vdb.get( 1 )
    10
    >>> vdb.end()
    >>> vdb.get_version()
    1

    >>> vdb.begin()
    >>> vdb.set( 2, 20 )
    >>> vdb.get( 2 )
    20
    >>> vdb.end()
    >>> vdb.get_version()
    2

    @see: L{rawdb.RawDB}, L{DB}
    """
    def __init__( self, data=None, journal_log=None ):
        """Create a new Versioned DB.

        @param data: raw data base.
        @type  data: @L{rawdb.RawDB}
        @param journal_log: journal log for this data base.
        @type  journal_log: @L{list}.

        It will get singleton instances of:
         - L{datamanager.DataManager}: coordinate access to data using locks.
         - L{rawdb.RawDB}: hold data.
         - L{transactionmanager.TransactionManager}: coordinate transactions,
           do commits and aborts/rollbacks.

        @rtype: L{VersionedDB}
        """
        if journal_log is None:
            journal_log = []
        else:
            try:
                journal_log[ 0 ]
            except IndexError:
                pass
            except Exception:
                raise TypeError( "journal_log must be subscriptable!" )

	DB.__init__( self, data )
	self.__journal = journal_log
        self.__journal_base = 0
    # __init__()


    def begin( self ):
	if self.in_transaction():
	    return
	else:
	    DB.begin( self )
	    self.__transaction_journal = []
    # begin()


    def end( self ):
	DB.end( self )
        je = self.__transaction_journal
        if je:
            self.__journal.append( tuple( je ) )
	self.__transaction_journal = None
    # end()


    def abort( self ):
	DB.abort( self )
	self.__transaction_journal = None
    # abort()


    def __changed( self, id, value ):
        """Internal method to increment transaction's version.

        It increases transaction's version and add a new entry to
        transaction's journal.

        @precondition: L{in_transaction}() must be true.

        @param id: changed id.
        @param value: new id value.
        """
	self.__transaction_journal.append( ( id, value ) )
    # __changed()


    def set( self, id, value ):
	DB.set( self, id, value )
	self.__changed( id, value )
    # set()
    __setitem__ = set


    def get_version( self ):
        """Returns current database version.

        @return: current version.
        @rtype: L{int}
        """
	return len( self.__journal ) + self.__journal_base
    # get_version()


    def diff( self, version1, version2=None ):
	"""Returns a dict with data changed from version1 to version2.

        This data can be passed to L{DB.__db_update__} or
        L{rawdb.RawDB.update}.

        @param version1: previous version.
        @type  version1: L{int}
        @param version2: next version. If C{None}, latest version is used.
        @type  version2: L{int}

        @return: dictionary with changed data.
        @rtype: L{dict}
	"""
	updated = {}
        for l in self.journal_entries( version1, version2 ):
            for id, value in l:
		updated[ id ] = value
	return updated
    # diff()


    def journal_entries( self, version1, version2=None ):
        """Return journal entries between versions.

        @param version1: previous version.
        @type  version1: L{int}
        @param version2: next version. If C{None}, latest version is used.
        @type  version2: L{int}

        @return: list of journal entries as pairs (version, value).
        @rtype: L{list} of L{tuple}s
        """
	if version2 is None:
	    version2 = self.get_version()

	if version1 > version2:
	    raise ValueError( "version1 must be less than version2" )

        version1 -= self.__journal_base
        version2 -= self.__journal_base

        if version1 < 0:
            version1 = 0
        if version2 < 0:
            return []

        return self.__journal[ version1 : version2 ]
    # journal_entries()


    def shrink_journal( self, size ):
	"""Shrink database journal to last 'size' entries.

        @note: After this command is called, you cannot get differences from
               forgotten versions.

        @postcondition: journal will have at most C{size} entries.

        @param size: how many entries to keep.
        @type  size: L{int}
	"""
        del self.__journal[ : - size ]
        self.__journal_base += size - 1
    # shrink_journal()


    def __str__( self ):
        """VersisonedDB representation.

        @rtype: L{str}
        """
        return "Version: %d\n%s\n%s\n" % \
               ( self.get_version(), DB.__str__( self ), self.__journal )
    # __str__()
# VersionedDB
